Android 触摸事件分发解析
前言
Android 中的界面是由一颗 view tree 组成的,从 Activity 开始向下分发触摸事件直到被目标 view 消费,这篇文章将带你详细了解事件从 viewRoot 接受到目标 view 消费的过程
代码基于 Android 7.0
基础知识
事件的传递对象 MotionEvent 有许多类型,通过 getAction 方法可以获取到当前事件的类型,主要有
- ACTION_DOWN 手指按下事件
- ACTION_MOVE 手指移动事件
- ACTION_UP 手指抬起事件
- ACTION_CANCEL 触摸取消事件
- …
以及一些额外的手势,比如说用于多指操控的 ACTION_POINT_XX 事件等,但绝大部分的控件只需要关心这四个事件就行了,一般的触摸事件如下图所示
Android 的源码量太庞大了,所以在解析过程中会选择性的略去一部分非关键性流程代码
事件分发流程解析
ViewRootImpl 转发
如果想看触摸事件是如何从底层传递到应用层的话,我建议你去看看 gityuan 的关于一系列关于 Input 系统的博客,本文从 Input系统—事件处理全过程 这篇博客中在 ViewRootImpl 接收到数据后 deliverInputEvent 方法开始解析
ViewRootImpl.deliverInputEvent
1 | private void deliverInputEvent(QueuedInputEvent q) { |
简述一下过程,在 InputStage 中会持有下一个 InputStage 的引用,InputStage.deliver 分发时会在 InputStage.onProcess 中处理具体的逻辑,然后根据返回值设置下一个 InputStage.deliver 分发时是否需要处理的状态,知道这条调用链执行完毕,调用 finishInputEvent 回调给系统。沿着调用链查看对应的 onProcess 方法,发现在 ViewPostImeInputStage.onProcss 中会判断事件的来源是否是 SOURCE_CLASS_POINTER,是的话就会调用 ViewPostImeInputStage.processPointerEvent 方法处理手指事件
ViewPostImeInputStage.processPointerEvent 方法
1 | private int processPointerEvent(QueuedInputEvent q) { |
ViewRootImpl 中的 mView 变量其实持有的是 DecorView 的引用,而 DecorView 则是一个 Activity 布局中最顶层的控件,所以 DecorView 是作为一个根节点开发分发事件的
View.dispatchPointerEvent 方法
1 | public final boolean dispatchPointerEvent(MotionEvent event) { |
改方法是 final 修饰的,所以该方法无法被子类覆写,而且该方法是 @hide 的,开发者无法直接调用它。当 event 是一个触摸事件是就会调用 dispatchTouchEvent,DecorView 有覆写该方法。
Activity 的相关操作
DecorView.dispatchTouchEvent 方法
1 |
|
DecorView 会将 dispatchTouchEvent 交由 Window 中 持有的一个 Callback 对象去执行逻辑,去 Window 的唯一子类 PhoneWindow 中查看相关代码,发现这个 Callback 对象实际上就是宿主 Activity,继续追踪到 Activity.dispatchTouchEvent 方法
Activity.dispatchTouchEvent 方法
1 | public boolean dispatchTouchEvent(MotionEvent ev) { |
分发时,event 通过 PhoneWindow.superDispatchTouchEvent 传递到 DecorView.superDispatchTouchEvent 方法中,而 DecorView 调用了 super.dispatchTouchEvent(DecorView 覆写了 dispatchTouchEvent,上文也有提到,所以调用父类 ViewGroup 的 dispatchTouchEvent 方法)
由此可以知道 Activity 的处理逻辑,它会先于所有的 view 接收到触摸事件,在 dispatchTouchEvent 中只有 View 没有消费事件,才会执行 onTouchEvent
ViewGroup 的相关操作
追踪到 ViewGroup.dispatchTouchEvent 方法,这个方法里定义了事件沿着树状结构向下传递的主要规则
在看源码之前需要了解一些基本知识,一个触摸事件的周期由 down 事件开始,正常会以 up 事件结束,在这个周期内如果 view 消费了事件,那么后续事件都会直接传递进来,如果没有则不会接受到后续事件
ViewGroup.dispatchTouchEvent
1 |
|
上述流程中有三处调用了 dispatchTransformedTouchEvent 方法,从传参和方法字面意义可知,里面执行的是将处理后的 TouchEvent 传给 child.dispatchTouchEvent 来查看事件是否会被消费
ViewGroup.dispatchTransformedTouchEvent 方法
1 | private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, |
可以看到如果传入的 child 为空,则会调用 ViewGroup 的父类方法的 dispatchTouchEvent,相当于把 ViewGroup 当成普通的 View 处理了。如果传递给子 view 那么会根据子 view 能够处理对应的手指来拆分事件,并对事件的位置进行裁剪,使得子 view 获取到的事件位置是相对位置。
这就是 ViewGroup 的分发逻辑,简单来说一个触摸周期的 DOWN 事件来时,会判断是否取消或者拦截,都没有的话就根据从前到后的 view 顺序遍历子 view 找到能消费的 view 并记录下来,等到 MOVE 等后续事件来时就不需要再遍历了,只需要找到对应的消费了 DOWN 事件的 view 直接传递给他就行,直到 UP 或者 CANCEL 事件清除记录下来的 view。等到下一个周期从新开始。
View的相关操作
上节提到当 ViewGroup 在 DOWN 事件被拦截或者取消,或者找不到可以消费事件的子 view,那他就会调用 View.dispatchTouchEvent,或者当事件传递到最底层时也是由 View.dispatchTouchEvent 去处理的
View.dispatchTouchEvent 方法
1 | public boolean dispatchTouchEvent(MotionEvent event) { |
View 的 dispatchTouchEvent 相对比较简单,只是操作了一些额外的滑动事件处理,只要逻辑在于如果有 OnTouchListener 就交给 OnTouchListener.onTouch 处理,不消费则再去通过 onTouchEvent 方法处理,如果都没有消费意味着返回 false,那后续的事件是不会传进来的
可以用一张图来描述这个过程
结语
一个相对完整的触摸事件分发流程就解析完了,不仅仅是停留于纸面上的 API 调用关系,相信会对你自定义 ViewGroup 如何处理事件分发,解决滑动冲突会有很大的帮助